美团面试:说一说 ThreadLocal 原理?网友:难道面试不看源码不行?
>>号外:关注“Java精选”公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料、开源项目。
前言
上周我侥幸通过美团一面,岗位是java后端开发工程师。美团面试官给我进行了二面。面试过程中他问了ThreadLocal原理(上次问线程池,这次问ThreadLocal,美团爸爸这么喜欢线程安全机制么),今天详细讲一讲ThreadLocal原理。
ThreadLocal
ThreadLocal是线程的内部存储类,可以在指定线程内存储数据。只有指定线程可以得到存储数据。
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/
每个线程都有一个ThreadLocalMap的实例对象,并且通过ThreadLocal管理ThreadLocalMap。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
每个新线程都会实例化为一个ThreadLocalMap并且赋值给成员变量ThreadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
应用场景
当某些数据是以线程为作用域并且不同线程有不同数据副本时,考虑ThreadLocal。
无状态,副本变量独立后不影响业务逻辑的高并发场景。
如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决。
get()与set()
set()是调用ThreadLocalMap的set()实现的
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//getMap方法
ThreadLocalMap getMap(Thread t) {
//thred中维护了一个ThreadLocalMap
return t.threadLocals;
}
//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap
ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标是value存储的对应位置。
hreadLocalMaps是延迟构造的,因此只有在至少要放置一个条目时才创建。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap初始化时创建了默认长度是16的Entry数组。通过hashCode与length位运算确定索引值i。
每个Thread都有一个ThreadLocalMap类型。相当于每个线程Thread都有一个Entry型的数组table。而一切读取过程都是通过操作这个数组table进行的。
set() 方法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//通过&运算计算索引
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果存在key则覆盖
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//新建结点插入
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
将threadLocalHashCode与长度进行位运算得到索引。
threadLocalHashCode的代码如下:
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
由于是static变量,threadLocalHashCode在每次加载threadLocal类时会重新初始化,同时会自增一次,增加HASH_INCREMENT(斐波那契散列乘数,通过该数散列出来的结果会比较均匀)。
static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。
对于一个ThreadLocal来讲,他的索引值i是确定的。对于不同线程,同一个threadlocal对应的是不同table的同一下标,即是table[i],不同线程之间的table是相互独立的。
get() 方法
计算索引,直接取出
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
remove() 方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
线程隔离特性
线程隔离特性,只有在线程内才能获取到对应的值,线程外不能访问。
Synchronized是通过线程等待,牺牲时间来解决访问冲突
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突
内存泄露问题
存在内存泄露问题,每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
Demo程序
import java.util.concurrent.atomic.AtomicInteger;
/**
* <h3>Exper1</h3>
* <p>ThreadLocalId</p>
*
* @author : cxc
* @date : 2020-04-01 23:48
**/
public class ThreadLocalId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal <Integer> threadId =
new ThreadLocal<Integer>()
{
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
public static void remove() {
threadId.remove();
}
}
/**
* <h3>Exper1</h3>
* <p></p>
*
* @author : cxc
* @date : 2020-04-02 00:07
**/
public class ThreadLocalMain {
private static void incrementSameThreadId(){
try{
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread()
+"_"+i+",threadId:"+
ThreadLocalId.get());
}
}finally {
ThreadLocalId.remove();
}
}
public static void main(String[] args) {
incrementSameThreadId();
new Thread(new Runnable() {
@Override
public void run() {
incrementSameThreadId();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
incrementSameThreadId();
}
}).start();
}
}
总结
咱们玩归玩,闹归闹,别拿面试开玩笑。
ThreadLocal的原理在面试中几乎被问烂了。Thread的私有数据是存储在ThreadLocalMap,通过ThreadLoacl进行管理。要了解ThreadLocal的原理,最好多阅读几遍源码,尤其是ThreadLocalMap的源码部分。大家面试前要把知识点记牢。
作者:天才程序YUAN
blog.csdn.net/JAck_chen0309/article/details/105257331
如何理解 Java 开发中 Cookie、Session、Token、JWT 登录认证授权,它们有什么区别?
Spring 框架 @Component、@Service 等注解是如何被解析的?
从零开始实现 Spring Boot 简易读写分离,其实也不难嘛!
Linux 中部署线上环境 Java 应用排查问题时常用命令汇总,值得收藏!
为什么 MySQL 唯一索引会导致死锁,“有心杀贼,无力回天”?
为什么 Redis 使用单线程,而新版本开始引入多线程,你什么看法?
程序员的忠告:为什么避免使用 SELECT * 查询,效率低?